using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;

// For DB
using com.db4o;
using com.db4o.query;
using Db4oTools;

namespace StormModelToKMLDB
{
    public delegate void LogDelegate(string txt);

    public class Database
    {
        private bool _defragNeeded;
        public event LogDelegate LogMessage;
        private Thread _cleanupThread;
        private string _fileName = "";
        private ObjectContainer _db;
        private int _cleanupIntervalMillis;
        private Thread _dbOpenThread;

        public Database(string fileName, int cleanupIntervalMinutes)
        {
            this._cleanupIntervalMillis = 60 * 1000 * cleanupIntervalMinutes;

            this._fileName = fileName;

            Db4o.Configure().ObjectClass(typeof(Storm)).CascadeOnActivate(true);
            Db4o.Configure().ObjectClass(typeof(Storm)).CascadeOnUpdate(true);
            Db4o.Configure().ObjectClass(typeof(Storm)).CascadeOnDelete(true);

            Db4o.Configure().ObjectClass(typeof(NHCDiscussion)).CascadeOnActivate(true);
            Db4o.Configure().ObjectClass(typeof(NHCDiscussion)).CascadeOnUpdate(true);
            Db4o.Configure().ObjectClass(typeof(NHCDiscussion)).CascadeOnDelete(true);

            Db4o.Configure().ObjectClass(typeof(Observation)).CascadeOnActivate(true);
            Db4o.Configure().ObjectClass(typeof(Observation)).CascadeOnUpdate(true);
            Db4o.Configure().ObjectClass(typeof(Observation)).CascadeOnDelete(true);

            Db4o.Configure().ObjectClass(typeof(TempBitmap)).CascadeOnActivate(true);
            Db4o.Configure().ObjectClass(typeof(TempBitmap)).CascadeOnUpdate(true);
            Db4o.Configure().ObjectClass(typeof(TempBitmap)).CascadeOnDelete(true);

            //Db4o.Configure().DiscardFreeSpace(31);
            this._dbOpenThread = new Thread(this.OpenDB);
            this._dbOpenThread.Start();
            
            this._cleanupThread = new Thread(new ThreadStart(this.Cleanup));
            this._cleanupThread.Name = "DatabaseCleanupThread";
            this._cleanupThread.IsBackground = true;
            this._cleanupThread.Start();
        }

        public bool Ready
        {
            get
            {
                return this._db != null;
            }
        }

        public bool InsertStormModel(Storm s)
        {
            // Don't put storms > 12hrs old into DB
            if ((DateTime.Now - s.DateTime) > new TimeSpan(12, 0, 0))
            {
                Log(string.Format("DB: Not inserting {0}{1}, age > 12hrs", s.ID, s.Name));
                return false;
            }

            // Now decide whether to insert or update the remaining storms
            lock (_db)
            {
                // Check if storm exists in db, if so then Update
                IList<Storm> result = this._db.Query<Storm>(delegate(Storm storm)
                {
                    return storm.ID == s.ID;
                });

                if (result.Count == 0)
                {
                    Log(string.Format("DB: Inserting {0}{1} {2}", new object[] { s.ID, s.Name, s.DateTime }));
                    //Console.WriteLine("Inserting {0}{1} {2}", s.ID, s.Name, s.DateTime);
                    _db.Set(s);
                }
                else
                {
                    // Update if s is newer
                    if (result[0].DateTime < s.DateTime)
                    {
                        Storm toUpdate = result[0];
                        Observation last = result[0].Initial;
                        Log(string.Format("DB: Updating {0}{1} : \n\t{2} > {3}", new object[] { s.ID, s.Name, s.DateTime, result[0].DateTime }));
                        //Console.WriteLine("Updating {0}{1} : \n\t{2}>{3}", s.ID, s.Name, s.DateTime, toUpdate.DateTime);
                        Storm.Update(toUpdate, s);
                        _db.Set(toUpdate);
                    }
                    else
                    {
                        Log(string.Format("DB: Not Updating {0}{1} : data not newer", new object[] { s.ID, s.Name }));
                        return false;
                    }
                }
                _db.Commit();
            }
            return true;
        }

        public void AddDiscussionToStorm(NHCDiscussion disc)
        {
            // Find storm that we need and update it with new discussion
            lock (_db)
            {
                // Check if storm exists in db, if so then Update
                IList<Storm> result = this._db.Query<Storm>(delegate(Storm storm)
                {
                    return storm.ID == disc.Id;
                });

                if (result.Count == 0)
                {
                    Log(string.Format("DB: No Storm to add discussion to for NHC disc:{0}{1}", new object[] { disc.Id, disc.DateTime }));
                }
                else
                {
                    Storm toUpdate = result[0];
                    if(toUpdate.AddDiscussion(disc))
                        Log(string.Format("DB: Adding Discussion {0}{1} for {2}", new object[] { disc.Id, disc.DateTime, toUpdate.Name }));
                    else
                        Log(string.Format("DB: Discussion {0}{1} for {2} is not newer", new object[] { disc.Id, disc.DateTime, toUpdate.Name }));
                    _db.Set(toUpdate);
                }
                _db.Commit();
            }
        }

        public List<Storm> GetCurrentStorms(TimeSpan maxAge)
        {
            List<Storm> ret = new List<Storm>();
            IList<Storm> storms = new List<Storm>();
            lock(_db)
            {
                storms = this._db.Query<Storm>(typeof(Storm));
            }
            foreach (Storm s in storms)
            {
                if ((DateTime.Now - s.DateTime) <= maxAge)
                    ret.Add(s);
            }
            return ret;
        }

        public bool InsertSST(TempBitmap tbm)
        {
            IList<TempBitmap> result = new List<TempBitmap>();
            lock (_db)
            {
                // Get List of TempMaps that satisfy parameters
                result = this._db.Query<TempBitmap>(delegate(TempBitmap query)
                {
                    return ((query.Lat == tbm.Lat) && (query.Lon == tbm.Lon) && (query.SpanDeg == tbm.SpanDeg));
                });

                if (result.Count > 0)
                {
                    // Replace if DateTime is newer
                    if (tbm.DateTime > result[0].DateTime)
                    {
                        TempBitmap toUpdate = result[0];
                        TempBitmap.Update(toUpdate, tbm);
                        Log(string.Format("DB: Updating SSTMap {0} ({1},{2}) : data is newer", new object[] { tbm.Filename, tbm.Lat, tbm.Lon }));
                        _db.Set(toUpdate);
                    }
                    else
                    {
                        Log(string.Format("DB: Not Updating SSTMap {0} ({1},{2}) : data not newer", new object[] { tbm.Filename, tbm.Lat, tbm.Lon }));
                        return false;
                    }
                }
                else
                {
                    // Just add a nonexistant one
                    Log(string.Format("DB: Inserting SSTMap {0} ({1},{2})", new object[] { tbm.Filename, tbm.Lat, tbm.Lon }));
                    _db.Set(tbm);
                }
                _db.Commit();
            }
            return true;
        }

        public TempBitmap GetCurrentSST(int latLL, int lonLL, int degSpan)
        {
            lock (_db)
            {
                // Get List of TempMaps that satisfy parameters
                IList<TempBitmap> result = this._db.Query<TempBitmap>(delegate(TempBitmap tbm)
                {
                    return ((tbm.Lat == latLL) && (tbm.Lon == lonLL) && (tbm.SpanDeg == degSpan));
                });

                if (result.Count > 0)
                    return (TempBitmap)result[0];
                else 
                    return null;
                
            }
        }

        public void Close()
        {
            this._cleanupThread.Interrupt();
            this._db.Close();
        }

        private void Cleanup()
        {
            try
            {
                while (true)
                {
                    // Database may not be open if it was in use from another app.
                    if (this._db != null)
                    {
                        // Delete old storms (12 hr)
                        lock (_db)
                        {
                            IList<Storm> storms = this._db.Query<Storm>(typeof(Storm));
                            foreach (Storm s in storms)
                            {
                                if ((DateTime.Now - s.DateTime) > new TimeSpan(24, 0, 0))
                                {
                                    Log(string.Format("DB: Deleting {0}{1} : \n\tage > 12hrs", new object[] { s.ID, s.Name }));
                                    //Console.WriteLine("Deleting {0}{1} : \n\tage > 12hrs", s.ID, s.Name);
                                    _db.Delete(s);
                                    this._defragNeeded = true;
                                }
                            }
                            _db.Commit();


                            // Now Defrag
                            _db.Close();
                            if (this._defragNeeded)
                            {
                                Log(string.Format("DB: Defragging database file : {0}", this._fileName));
                                Log("DB: Size before defrag: " + GetFileSize(this._fileName));
                                new Defragment().Run(_fileName, true);
                                Log("DB: Size after defrag : " + GetFileSize(this._fileName));
                                this._defragNeeded = false;
                            }
                            _db = Db4o.OpenFile(this._fileName);
                        }
                    }
                    Thread.Sleep(this._cleanupIntervalMillis);
                }
            }
            catch (Exception)
            {
                return;
            }
        }

        private void OpenDB()
        {
            while (_db == null)
            {
                this._db = Db4o.OpenFile(_fileName);
                if (_db == null)
                {
                    Log("Failed to open database file...may be in use by another process");
                    Thread.Sleep(1000);
                }
            }
            Log("\t ...Database " + _fileName + " opened succesfully");
        }
        private void Log(string txt)
        {
            if (LogMessage != null)
                LogMessage(txt);
        }

        private string GetFileSize(string fileName)
        {
            string ret = "";
            try
            {
                string[] sizes = new string[] { "Bytes", "KB", "MB", "GB" };
                FileInfo fi = new FileInfo(this._fileName);
                double len = fi.Length;
                int cnt = 0;
                while (len > 1023)
                {
                    len /= 1024;
                    cnt++;
                }
                ret = len.ToString("F2") + sizes[cnt];
            }
            catch (Exception)
            {
                return "??? Bytes";
            }

            return ret;
        }
    }
}
